在昨天的首頁中,我們完成了「今日營養統計」與「餐點清單」的顯示。
今天,我們要讓使用者能夠開啟「新增飲食記錄頁」,輸入餐點資料,並即時更新回首頁
完成「新增記錄」頁面,讓使用者可以輸入:
並透過 NavigationLink 進入新增畫面,填寫完畢後返回首頁更新清單。
目前我們的資料是放在DashboardViewModel裡面。但如果「新增記錄頁」也要能操作資料,就會導致兩個 ViewModel 之間耦合。
因此,我們要建立一個MealRepository來集中管理資料來源。這樣未來若改成使用 SwiftData,也只需要修改 Repository 的實作即可。
import Foundation
class MealRepository: ObservableObject {
static let shared = MealRepository()
@Published private(set) var records: [MealRecord] = []
private init() {}
func addMeal(name: String, calories: Int, category: MealCategory) {
let newMeal = MealRecord(
name: name,
calories: calories,
category: category,
date: Date()
)
records.append(newMeal)
}
}
private(set)
:讓外部可以讀但不能直接改,確保資料一致性。@Published
:當資料更新時,畫面會自動重新整理。static let
:建立單例(Singleton),確保資料來源唯一。DashboardViewModel 只需要監聽 Repository 的變化,當 Repository 新增資料時,就會即時更新。
class DashboardViewModel: ObservableObject {
@Published var records: [MealRecord] = []
private let repository = MealRepository.shared
init() {
// 監聽 repository 的變化
repository.$records
.receive(on: RunLoop.main)
.assign(to: &$records)
}
var totalCalories: Int {
records.reduce(0) { $0 + $1.calories }
}
// 假資料
var protein: Int { 60 }
var carbs: Int { 150 }
var fat: Int { 40 }
}
接著我們來建立 AddMealViewModel,專門處理「新增餐點」邏輯,包含表單輸入與驗證。
class AddMealViewModel: ObservableObject {
@Published var mealName: String = ""
@Published var mealCalories: String = ""
@Published var mealCategory: MealCategory = .breakfast
@Published var showAlert: Bool = false
@Published var addSuccess: Bool = false
private let repository = MealRepository.shared
func addMeal() {
guard !mealName.trimmingCharacters(in: .whitespaces).isEmpty else {
showAlert = true
return
}
guard let calories = Int(mealCalories),
!mealCalories.trimmingCharacters(in: .whitespaces).isEmpty else {
showAlert = true
return
}
repository.addMeal(
name: mealName,
calories: calories,
category: mealCategory
)
// 清空輸入
mealName = ""
mealCalories = ""
mealCategory = .breakfast
addSuccess = true
}
}
addSuccess
通知畫面自動關閉。我們建立一個簡單的輸入表單 AddMealView
:
struct AddMealView: View {
@Environment(\.dismiss) private var dismiss
@StateObject private var viewModel = AddMealViewModel()
var body: some View {
Form {
Section(header: Text("餐點資訊")) {
TextField("餐點名稱", text: $viewModel.mealName)
TextField("卡路里", text: $viewModel.mealCalories)
.keyboardType(.numberPad)
Picker("餐別", selection: $viewModel.mealCategory) {
ForEach(MealCategory.allCases, id: \.self) { category in
Text(category.rawValue)
}
}
}
Section {
Button("儲存") {
viewModel.addMeal()
}
.frame(maxWidth: .infinity, alignment: .center)
}
}
.alert("請輸入完整資訊", isPresented: $viewModel.showAlert) {
Button("確定", role: .cancel) {}
}
.onChange(of: viewModel.addSuccess) { _, success in
if success {
viewModel.addSuccess = false
dismiss()
}
}
.navigationTitle("新增記錄")
}
}
#Preview {
AddMealView()
}
.onChange(of:)
監聽 viewModel.addSuccess
,成功後自動關閉畫面。回到 DashboardView
,我們將「新增記錄」按鈕改成 NavigationLink
,讓使用者能切換頁面。
Section {
NavigationLink(destination: AddMealView(viewModel: viewModel)) {
Label("新增記錄", systemImage: "plus.circle.fill")
.font(.headline)
.foregroundColor(.blue)
}
.frame(maxWidth: .infinity, alignment: .center)
}
完成後,整個流程如下:
今天我們完成了飲食記錄 App 的第二個核心功能:
明天,我們將繼續擴充 App 功能,加入「飲食紀錄清單頁」與「資料持久化(SwiftData)」,讓資料在關閉 App 後也能保留不遺失。